/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.newatlanta.commons.vfs.provider.gae;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.CodeSigner;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.vfs.FileContent;
import org.apache.commons.vfs.FileObject;
import org.apache.commons.vfs.FileSystemException;
import org.apache.commons.vfs.FileSystemManager;
import org.apache.commons.vfs.NameScope;

import com.newatlanta.commons.vfs.provider.gae.jar.GaeJarFileObject;
import com.newatlanta.commons.vfs.provider.gae.jar.GaeJarFileObjectAdapter;
import com.newatlanta.commons.vfs.provider.gae.jar.GaeJarFileSystem;

/**
 * A class loader that can load classes and resources from a search path VFS
 * FileObjects refering both to folders and JAR files. Any FileObject of type
 * FileType.FILE is asumed to be a JAR and is opened by creating a layered file
 * system with the "jar" scheme.
 * 
 * @author <a href="mailto:brian@mmmanager.org">Brian Olsen</a>
 * @version $Revision: 804548 $ $Date: 2009-08-16 04:12:32 +0200 (Dom, 16 Ago
 *          2009) $
 * @see FileSystemManager#createFileSystem
 */
public class GaeClassLoader extends ClassLoader {

	@SuppressWarnings("unused")
	private static final Log LOG = LogFactory.getLog(GaeClassLoader.class);

	protected static class Resource {
		private final FileObject root;
		private final FileObject resource;
		private final String packageName;

		/**
		 * Creates a new instance.
		 * 
		 * @param root
		 *            The code source FileObject.
		 * @param resource
		 *            The resource of the FileObject.
		 */
		public Resource(final String name, final FileObject root,
				final FileObject resource) throws FileSystemException {
			this.root = root;
			this.resource = resource;

			final int pos = name.lastIndexOf('/');
			if (pos == -1) {
				packageName = null;
			} else {
				packageName = name.substring(0, pos).replace('/', '.');
			}
		}

		/**
		 * Returns the URL of the resource.
		 */
		public URI getURL() throws FileSystemException {
			return resource.getURI();
		}

		/**
		 * Returns the name of the package containing the resource.
		 */
		public String getPackageName() {
			return packageName;
		}

		/**
		 * Returns an attribute of the package containing the resource.
		 */
		public String getPackageAttribute(final Attributes.Name attrName)
				throws FileSystemException {
			Manifest mf = ((GaeJarFileSystem) root.getFileSystem())
					.getManifest();

			return (String) mf.getMainAttributes()
					.getValue(attrName.toString());
		}

		/**
		 * Returns the FileObject of the resource.
		 */
		public FileObject getFileObject() {
			return resource;
		}

		/**
		 * Returns the code source as an URL.
		 */
		public URI getCodeSourceURL() throws FileSystemException {
			return root.getURI();
		}

		/**
		 * Returns the data for this resource as a byte array.
		 */
		public final GaeJarFileObject.Buffer getBuffer() throws IOException {
			GaeJarFileObject.Buffer content = ((GaeJarFileObject) resource).getBuffer(); // FileUtil.getContent(resource);
			return content;
		}
		
		public Certificate[] getCertificates() throws FileSystemException {
			return resource.getContent().getCertificates();
		}

		public CodeSigner[] getCodeSigners() {
			return ((GaeJarFileObject) resource).getCodeSigners();
		}
	}

	public final ArrayList<FileObject> resources = new ArrayList<FileObject>();

	/**
	 * Constructors a new VFSClassLoader for the given file.
	 * 
	 * @param file
	 *            the file to load the classes and resources from.
	 * @param manager
	 *            the FileManager to use when trying create a layered Jar file
	 *            system.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	public GaeClassLoader(final FileObject file, final FileSystemManager manager)
			throws FileSystemException {
		this(new FileObject[] { file }, manager, null);
	}

	/**
	 * Constructors a new VFSClassLoader for the given file.
	 * 
	 * @param file
	 *            the file to load the classes and resources from.
	 * @param manager
	 *            the FileManager to use when trying create a layered Jar file
	 *            system.
	 * @param parent
	 *            the parent class loader for delegation.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	public GaeClassLoader(final FileObject file,
			final FileSystemManager manager, final ClassLoader parent)
			throws FileSystemException {
		this(new FileObject[] { file }, manager, parent);
	}

	/**
	 * Constructors a new VFSClassLoader for the given files. The files will be
	 * searched in the order specified.
	 * 
	 * @param files
	 *            the files to load the classes and resources from.
	 * @param manager
	 *            the FileManager to use when trying create a layered Jar file
	 *            system.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	public GaeClassLoader(final FileObject[] files,
			final FileSystemManager manager) throws FileSystemException {
		this(files, manager, null);
	}

	/**
	 * Constructors a new VFSClassLoader for the given FileObjects. The
	 * FileObjects will be searched in the order specified.
	 * 
	 * @param files
	 *            the FileObjects to load the classes and resources from.
	 * @param manager
	 *            the FileManager to use when trying create a layered Jar file
	 *            system.
	 * @param parent
	 *            the parent class loader for delegation.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	public GaeClassLoader(final FileObject[] files,
			final FileSystemManager manager, final ClassLoader parent)
			throws FileSystemException {
		super(parent);
		addFileObjects(manager, files);

	}

	@Override
	public InputStream getResourceAsStream(String name) {
		try {
			final Resource res = loadResource(name);
			if (res != null) {
				final FileContent content = res.getFileObject().getContent();
				return content.getInputStream();
			}
		} catch (final Exception mue) {
			// Ignore
			// TODO - report?
		}

		return null;
	}

	public void addFileObjects(FileSystemManager manager, FileObject[] files)
			throws FileSystemException {
		{
			for (int i = 0; i < files.length; i++) {
				FileObject file = files[i];
				if (!file.exists()) {
					// Does not exist - skip
					continue;
				}
				if (!(file instanceof GaeFileObject)) {
					// not valid type
				}

				resources.add((file instanceof GaeJarFileObject) ? file
						: new GaeJarFileObjectAdapter((GaeFileObject) file));
			}
		}
	}

	/**
	 * Searches through the search path of for the first class or resource with
	 * specified name.
	 * 
	 * @param name
	 *            The resource to load.
	 * @return The Resource.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	protected Resource loadResource(final String name) throws FileSystemException {
		final Iterator<FileObject> it = resources.iterator();
		while (it.hasNext()) {
			final FileObject baseFile = it.next();
			final FileObject file = 
					baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF);
			if (file != null /* file.exists() */) {

				return new Resource(name, baseFile, file);
			}
		}

		return null;
	}
	
	/**
	 * Provide access to the file objects this class loader represents.
	 * 
	 * @return An array of FileObjects.
	 */
	public FileObject[] getFileObjects() {
		return (FileObject[]) resources
				.toArray(new FileObject[resources.size()]);
	}

	/**
	 * Finds and loads the class with the specified name from the search path.
	 * 
	 * @throws ClassNotFoundException
	 *             if the class is not found.
	 */
	@Override
	protected Class<?> findClass(final String name)
			throws ClassNotFoundException {
		try {
			final String path = name.replace('.', '/').concat(".class");
			final Resource res = loadResource(path);
			if (res == null) {
				throw new ClassNotFoundException(name);
			}
			return defineClass(name, res);
		} catch (final IOException ioe) {
			throw new ClassNotFoundException(name, ioe);
		}
	}

	/**
	 * Loads and verifies the class with name and located with res.
	 */
	private Class<?> defineClass(final String name, final Resource res)	throws IOException {
		final String pkgName = res.getPackageName();
		if (pkgName != null) {
			final Package pkg = getPackage(pkgName);
			if (pkg != null) {
				if (pkg.isSealed()) {
					try {
						final URL url = res.getCodeSourceURL().toURL();
						if (!pkg.isSealed(url)) {
							throw new FileSystemException(
									"vfs.impl/pkg-sealed-other-url", pkgName);
						}
					}
					catch( MalformedURLException e ) {
						LOG.warn( String.format("error getting url from [%s]. IGNORED!", res.getCodeSourceURL()));
					}
				} else {
					if (isSealed(res)) {
						throw new FileSystemException(
								"vfs.impl/pkg-sealing-unsealed", pkgName);
					}
				}
			} else {
				definePackage(pkgName, res);
			}
		}

		final GaeJarFileObject.Buffer buffer = res.getBuffer();
		// final Certificate[] certs = res.getCertificates();
		// final CodeSource cs = new CodeSource(url, certs);
		return defineClass(name, buffer.getBytes(), 0, buffer.getLength() /* , cs */);
	}

	/**
	 * Returns true if the we should seal the package where res resides.
	 */
	private boolean isSealed(final Resource res) throws FileSystemException {
		final String sealed = res.getPackageAttribute(Attributes.Name.SEALED);
		return "true".equalsIgnoreCase(sealed);
	}

	/**
	 * Reads attributes for the package and defines it.
	 */
	private Package definePackage(final String name, final Resource res)
			throws FileSystemException {
		final String specTitle = res
				.getPackageAttribute(Name.SPECIFICATION_TITLE);
		final String specVendor = res
				.getPackageAttribute(Attributes.Name.SPECIFICATION_VENDOR);
		final String specVersion = res
				.getPackageAttribute(Name.SPECIFICATION_VERSION);
		final String implTitle = res
				.getPackageAttribute(Name.IMPLEMENTATION_TITLE);
		final String implVendor = res
				.getPackageAttribute(Name.IMPLEMENTATION_VENDOR);
		final String implVersion = res
				.getPackageAttribute(Name.IMPLEMENTATION_VERSION);

		URL sealBase;
		if (isSealed(res)) {
			try {
				sealBase = res.getCodeSourceURL().toURL();
			} catch (MalformedURLException e) {
				sealBase = null;
			}
		} else {
			sealBase = null;
		}

		return definePackage(name, specTitle, specVersion, specVendor,
				implTitle, implVersion, implVendor, sealBase);
	}

	/**
	 * Calls super.getPermissions both for the code source and also adds the
	 * permissions granted to the parent layers.
	 * 
	 * @param cs
	 *            the CodeSource.
	 * @return The PermissionCollections.
	 */
	/*
	 * @Override protected PermissionCollection getPermissions(final CodeSource
	 * cs) { try { final String url = cs.getLocation().toString(); FileObject
	 * file = lookupFileObject(url); if (file == null) { return
	 * super.getPermissions(cs); }
	 * 
	 * FileObject parentLayer = file.getFileSystem().getParentLayer(); if
	 * (parentLayer == null) { return super.getPermissions(cs); }
	 * 
	 * Permissions combi = new Permissions(); PermissionCollection permCollect =
	 * super.getPermissions(cs); copyPermissions(permCollect, combi);
	 * 
	 * for (FileObject parent = parentLayer; parent != null; parent =
	 * parent.getFileSystem().getParentLayer()) { final CodeSource parentcs =
	 * new CodeSource(parent.getURL(), parent.getContent().getCertificates());
	 * permCollect = super.getPermissions(parentcs);
	 * copyPermissions(permCollect, combi); }
	 * 
	 * return combi; } catch (final FileSystemException fse) { throw new
	 * SecurityException(fse.getMessage()); } }
	 */

	/**
	 * Copies the permissions from src to dest.
	 * 
	 * @param src
	 *            The source PermissionCollection.
	 * @param dest
	 *            The destination PermissionCollection.
	 */
	protected void copyPermissions(final PermissionCollection src,
			final PermissionCollection dest) {
		for (Enumeration<Permission> elem = src.elements(); elem
				.hasMoreElements();) {
			final Permission permission = elem.nextElement();
			dest.add(permission);
		}
	}


	/**
	 * Finds the resource with the specified name from the search path. This
	 * returns null if the resource is not found.
	 * 
	 * @param name
	 *            The resource name.
	 * @return The URL that matches the resource.
	 */
	protected URL findResource(final String name) {
		try {
			final Resource res = loadResource(name);
			if (res != null) {
				return res.getURL().toURL();
			}
		} catch (final Exception mue) {
			// Ignore
			LOG.warn( String.format("error finding resource [%s]", name), mue);
		}

		return null;
	}

	/**
	 * Returns an Enumeration of all the resources in the search path with the
	 * specified name. TODO - Implement this.
	 * 
	 * @param name
	 *            The resources to find.
	 * @return An Enumeration of the resources associated with the name.
	 */
	@Override
	protected Enumeration<URL> findResources(final String name) {
		final java.util.List<URL> result = new java.util.ArrayList<URL>( resources.size());
		
		for( FileObject baseFile : resources ) {
			
			try {
				final FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF);
				if (file != null /* file.exists() */) {

					result.add( file.getURI().toURL() );
				}
			} catch (FileSystemException e) {
				LOG.warn( String.format("error resolving file [%s]", name));
			} catch (MalformedURLException e) {
				LOG.warn( String.format("error resolving file [%s]", name));
			}
			
		}
		
		return Collections.enumeration(result);
	}

}